package net.kleinhenz.droidshop.connector;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.ws.BindingProvider;

import org.apache.commons.lang.NotImplementedException;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.ws.security.handler.WSHandlerConstants;

import com.amazon.webservices.awsecommerceservice._2009_07_01.AWSECommerceService;
import com.amazon.webservices.awsecommerceservice._2009_07_01.AWSECommerceServicePortType;
import com.amazon.webservices.awsecommerceservice._2009_07_01.EditorialReview;
import com.amazon.webservices.awsecommerceservice._2009_07_01.Item;
import com.amazon.webservices.awsecommerceservice._2009_07_01.ItemAttributes;
import com.amazon.webservices.awsecommerceservice._2009_07_01.ItemLookup;
import com.amazon.webservices.awsecommerceservice._2009_07_01.ItemLookupRequest;
import com.amazon.webservices.awsecommerceservice._2009_07_01.ItemLookupResponse;
import com.amazon.webservices.awsecommerceservice._2009_07_01.ItemSearch;
import com.amazon.webservices.awsecommerceservice._2009_07_01.ItemSearchRequest;
import com.amazon.webservices.awsecommerceservice._2009_07_01.ItemSearchResponse;
import com.amazon.webservices.awsecommerceservice._2009_07_01.Offer;

import net.kleinhenz.droidshop.model.Locale;
import net.kleinhenz.droidshop.system.ApplicationConfiguration;
import net.kleinhenz.schemas.droidshop.Price;
import net.kleinhenz.schemas.droidshop.PriceList;
import net.kleinhenz.schemas.droidshop.Product;
import net.kleinhenz.schemas.droidshop.ProductList;
import net.kleinhenz.schemas.droidshop.Review;
import net.kleinhenz.schemas.droidshop.ReviewList;

public class Amazon extends CachingResource implements ProductInfoResource, OfferResource
{
	public static final String ACCESS_KEY = ApplicationConfiguration.getConfiguration(ApplicationConfiguration.KEY_AMAZON_ACCESSKEY);
	public static final String ASSOCIATE_ID = ApplicationConfiguration.getConfiguration(ApplicationConfiguration.KEY_AMAZON_ASSOCIATEID);
	public static final int MAX_REVIEW_PAGES = Integer.parseInt(ApplicationConfiguration.getConfiguration(ApplicationConfiguration.KEY_AMAZON_MAX_REVIEW_PAGES));
	public static final int MAX_PRICE_PAGES = Integer.parseInt(ApplicationConfiguration.getConfiguration(ApplicationConfiguration.KEY_AMAZON_MAX_REVIEW_PAGES));
	
	private Map<String, AWSECommerceServicePortType> amazons = null;
	private String[] amazonUrls = new String[] {
			Locale.LOCALE_DE, "https://ecs.amazonaws.de/onca/soap?Service=AWSECommerceService",
			Locale.LOCALE_UK, "https://ecs.amazonaws.co.uk/onca/soap?Service=AWSECommerceService",
			Locale.LOCALE_US, "https://ecs.amazonaws.com/onca/soap?Service=AWSECommerceService",			
	};
	
	public Amazon()
	{
		amazons = new HashMap<String, AWSECommerceServicePortType>();

		for (int i=0; i<amazonUrls.length; i+=2)
		{
			AWSECommerceService thisAmazonService = new AWSECommerceService(Amazon.class.getResource("/AWSECommerceService.wsdl"));
			AWSECommerceServicePortType thisAmazonPort = thisAmazonService.getAWSECommerceServicePort();
			((BindingProvider)thisAmazonPort).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, amazonUrls[i+1]); 
			
			// WS-Security stuff follows
			org.apache.cxf.endpoint.Client client = org.apache.cxf.frontend.ClientProxy.getClient(thisAmazonPort); 
			org.apache.cxf.endpoint.Endpoint cxfEndpoint = client.getEndpoint(); 

			// don't allow chunking
			HTTPConduit httpConduit = (HTTPConduit)client.getConduit();
			HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();
			httpClientPolicy.setAllowChunking(false);
			httpConduit.setClient(httpClientPolicy);

			// add interceptor
			Map<String,Object> outProps = new HashMap<String,Object>(); 			 
			WSS4JOutInterceptor wssOut = new WSS4JOutInterceptor(outProps); 
			cxfEndpoint.getOutInterceptors().add(wssOut); 

			// configure interceptor
			outProps.put(WSHandlerConstants.ACTION, WSHandlerConstants.TIMESTAMP + " " + WSHandlerConstants.SIGNATURE); 
			outProps.put(WSHandlerConstants.USER, "my_amazon_cert"); 
			outProps.put(WSHandlerConstants.PW_CALLBACK_CLASS, ClientCallbackHandler.class.getName()); 
			outProps.put(WSHandlerConstants.SIG_PROP_FILE, "amazonsecurity.properties");
			outProps.put(WSHandlerConstants.SIG_KEY_ID, "DirectReference");
			
			// add configured port to list
			amazons.put(amazonUrls[i], thisAmazonPort);
		}
	}
	
	@Override
	public ProductList searchProducts(String keywords, String locale)
	{
		ProductList list = null;
		
		// check cache
		Object cacheEntry = getCache(keywords, locale);
		if (cacheEntry!=null && cacheEntry instanceof ProductList)
			return (ProductList)cacheEntry;
		else
			list = new ProductList();

		// construct search request
		ItemSearch req = new ItemSearch();
		req.setAWSAccessKeyId(ACCESS_KEY);
		ItemSearchRequest r = new ItemSearchRequest();
		req.setAssociateTag(ASSOCIATE_ID);
		r.setSearchIndex("Blended");
		r.setKeywords(keywords);
		r.getResponseGroup().add("ItemAttributes,SalesRank");
		req.getRequest().add(r);
				
		// order peer list from locale preference
		List<AWSECommerceServicePortType> orderedAmazons = new ArrayList<AWSECommerceServicePortType>();
		for (String thisAmazon : amazons.keySet())
			if (thisAmazon.equals(locale.toUpperCase()))
				orderedAmazons.add(0, amazons.get(thisAmazon));
			else
				orderedAmazons.add(amazons.get(thisAmazon));
		
		// aggregate all amazon results across all locales
		List<Item> amazonResults = new ArrayList<Item>();
		for (AWSECommerceServicePortType amazon : orderedAmazons)
		{
			ItemSearchResponse resp = amazon.itemSearch(req);	
			for (Item thisItem : resp.getItems().get(0).getItem())
				amazonResults.add(thisItem);
		}

		// sort results by sales rank
		Collections.sort(amazonResults, new Comparator<Item>() {
            public int compare(Item o1, Item o2) 
            {
            	if (o1.getSalesRank() == null)
            		return Integer.MAX_VALUE;
            	else if (o2.getSalesRank() == null)
            		return Integer.MIN_VALUE;
                    	else
                    		return Integer.parseInt(o1.getSalesRank()) - Integer.parseInt(o2.getSalesRank());
            }
		});
		
		// parse list and unify results
		for (Item thisItem : amazonResults)
		{
			Product thisProduct = findByASIN(list, thisItem.getASIN());
			if (thisProduct==null)
			{
				thisProduct = new Product();
				list.getProduct().add(thisProduct);
			}

			// fetch base data only
			if (thisProduct.getAsin()==null)
				thisProduct.setAsin(thisItem.getASIN());
			if (thisProduct.getEan()==null)
				thisProduct.setEan(thisItem.getItemAttributes().getEAN());
			if (thisProduct.getName()==null)
				thisProduct.setName(thisItem.getItemAttributes().getTitle());
			if (thisProduct.getAuthor()==null)
				thisProduct.setAuthor(findAuthor(thisItem));
			if (thisProduct.getDetaillink()==null)
				thisProduct.setDetaillink(thisItem.getDetailPageURL());
			if (thisProduct.getType()==null)
				thisProduct.setType(thisItem.getItemAttributes().getProductGroup());
			if (thisProduct.getImagelink()==null && thisItem.getLargeImage()!=null)
				thisProduct.setImagelink(thisItem.getLargeImage().getURL());
		}
		
		// add to cache
		putCache(keywords, locale, list);
		
		return list;
	}	

	@Override
	public Product lookupProduct(String ean, String locale) 
	{
		// check cache
		Object cacheEntry = getCache(ean, locale);
		if (cacheEntry!=null && cacheEntry instanceof Product)
			return (Product)cacheEntry;
		
		// construct lookup request
		ItemLookup req = new ItemLookup();
		req.setAWSAccessKeyId(ACCESS_KEY);
		ItemLookupRequest r = new ItemLookupRequest();
		req.setAssociateTag(ASSOCIATE_ID);
		r.setIdType("EAN");
		r.getResponseGroup().add("ItemAttributes,EditorialReview,Reviews,OfferFull,Images");
		r.setMerchantId("All");
		r.getItemId().add(ean);
		r.setSearchIndex("Blended");
		r.setOfferPage(BigInteger.valueOf(1));
		req.getRequest().add(r);
		
		List<AWSECommerceServicePortType> orderedAmazons = new ArrayList<AWSECommerceServicePortType>();
		for (String thisAmazon : amazons.keySet())
			if (thisAmazon.equals(locale.toUpperCase()))
				orderedAmazons.add(0, amazons.get(thisAmazon));
			else
				orderedAmazons.add(amazons.get(thisAmazon));
		
		// aggregate all amazon results across all locales
		List<Item> amazonResults = new ArrayList<Item>();
		for (AWSECommerceServicePortType amazon : orderedAmazons)
		{
			ItemLookupResponse resp = amazon.itemLookup(req);	
			for (Item thisItem : resp.getItems().get(0).getItem())
				amazonResults.add(thisItem);
		}
		
		// parse list and unify results
		Product product = blendResults(amazonResults);

		// add to cache
		putCache(ean, locale, product);
		
		return product;
	}
	
	@Override
	public List<Price> lookupOffersByEAN(String ean, String locale)
	{
		Product product = lookupProduct(ean, locale);
		if (product==null)
			return Collections.emptyList();
		else
			return product.getPrices().getPrice();
	}

	@Override
	public List<Price> lookupOffersByKeywords(String keywords, String locale)
	{
		throw new NotImplementedException("Amazon connector does not support offer retrieval by keywords. This would be too slow.");
	}

	// Support methods follow
	
	private Product blendResults(List<Item> amazonResults) 
	{
		Product thisProduct = new Product();
		for (Item thisItem : amazonResults)
		{
			updateBaseData(thisProduct, thisItem);
			updateReviews(thisProduct, thisItem, findLocale(thisItem));
			updatePrices(thisProduct, thisItem, findLocale(thisItem));
		}
		return thisProduct;
	}

	private String findLocale(Item item)
	{
		if (item!=null && item.getDetailPageURL()!=null)
		{	
			String url = item.getDetailPageURL();
			if (url.toLowerCase().contains("amazon.de"))
					return Locale.LOCALE_DE;
			else if (url.toLowerCase().contains("amazon.com"))
					return Locale.LOCALE_US;
			else if (url.toLowerCase().contains("amazon.co.uk"))
					return Locale.LOCALE_UK;
			else return null;
		}
		else
			return null;
	}

	private Product findByASIN(ProductList list, String asin) 
	{
		for (Product thisProduct : list.getProduct())
		{
			if (thisProduct.getAsin()!=null && thisProduct.getAsin().equals(asin))
				return thisProduct;
		}
		return null;
	}

	private void updateBaseData(Product target, Item source)
	{
		// fetch base data
		if (target.getAsin()==null)
			target.setAsin(source.getASIN());
		if (target.getEan()==null)
			target.setEan(source.getItemAttributes().getEAN());
		if (target.getName()==null)
			target.setName(source.getItemAttributes().getTitle());
		if (target.getAuthor()==null)
			target.setAuthor(findAuthor(source));
		if (target.getDetaillink()==null)
			target.setDetaillink(source.getDetailPageURL());
		if (target.getType()==null)
			target.setType(source.getItemAttributes().getProductGroup());
		if (target.getImagelink()==null && source.getLargeImage()!=null)
			target.setImagelink(source.getLargeImage().getURL());
		
		// fetch description
		if (source.getEditorialReviews()!=null && source.getEditorialReviews().getEditorialReview()!=null && target.getDescription()==null)
			target.setDescription(findProductDescription(source.getEditorialReviews().getEditorialReview()));						
	}
		
	private void updateReviews(Product target, Item source, String locale)
	{
		if (target.getReviews()==null)
			target.setReviews(new ReviewList());
		if (source.getCustomerReviews()!=null && source.getCustomerReviews().getTotalReviews().intValue()>0)
		{
			target.getReviews().getReview().addAll(getReviews(source, amazons.get(locale)));
			target.setStars(source.getCustomerReviews().getAverageRating().doubleValue());
		}
	}
	
	private void updatePrices(Product target, Item source, String locale)
	{
		if (target.getPrices()==null)
			target.setPrices(new PriceList());
		if (source.getOffers()!=null && source.getOffers().getTotalOffers().intValue()>0)
			target.getPrices().getPrice().addAll(getPrices(source, amazons.get(locale)));
	}
	
	private List<Price> getPrices(Item source, AWSECommerceServicePortType port)
	{
		return getPrices(source.getASIN(), source.getDetailPageURL(), port);
	}
	
	private List<Price> getPrices(String asin, String detailPageURL, AWSECommerceServicePortType port)
	{
		List<Price> result = new ArrayList<Price>();

		// construct lookup request
		ItemLookup req = new ItemLookup();
		req.setAWSAccessKeyId(ACCESS_KEY);
		ItemLookupRequest r = new ItemLookupRequest();
		req.setAssociateTag(ASSOCIATE_ID);
		r.setIdType("ASIN");
		r.getResponseGroup().add("OfferFull");
		r.setMerchantId("All");
		r.getItemId().add(asin);
		r.setOfferPage(BigInteger.valueOf(1));
		req.getRequest().add(r);
		ItemLookupResponse resp = port.itemLookup(req);

		List<Offer> offerBuf = new ArrayList<Offer>();
		if (!resp.getItems().get(0).getItem().isEmpty())
		{
			offerBuf.addAll(resp.getItems().get(0).getItem().get(0).getOffers().getOffer());
			int pages = resp.getItems().get(0).getItem().get(0).getOffers().getTotalOfferPages().intValue();
			pages = MAX_PRICE_PAGES==0||pages<MAX_PRICE_PAGES?pages:MAX_PRICE_PAGES;

			for (int i=2; i<pages; i++)
			{
				r.setOfferPage(BigInteger.valueOf(i));
				resp = port.itemLookup(req);
				for (Item thisItem : resp.getItems().get(0).getItem())
					offerBuf.addAll(thisItem.getOffers().getOffer());
			}
		}
		
		for (Offer thisOffer : offerBuf)
		{
			Price price = new Price();
			price.setMerchant(thisOffer.getMerchant().getName());
			price.setFormattedprice(thisOffer.getOfferListing().get(0).getPrice().getFormattedPrice());
			price.setAvailability(thisOffer.getOfferListing().get(0).getAvailability());
			price.setBuylink(detailPageURL);
			result.add(price);
		}				

		return result;
	}

	private List<Review> getReviews(Item source, AWSECommerceServicePortType port)
	{
		List<Review> result = new ArrayList<Review>();

		// construct lookup request
		ItemLookup req = new ItemLookup();
		req.setAWSAccessKeyId(ACCESS_KEY);
		ItemLookupRequest r = new ItemLookupRequest();
		req.setAssociateTag(ASSOCIATE_ID);
		r.setIdType("ASIN");
		r.getResponseGroup().add("Reviews");
		r.getItemId().add(source.getASIN());
		r.setReviewPage(BigInteger.valueOf(1));
		req.getRequest().add(r);
		ItemLookupResponse resp = port.itemLookup(req);

		List<com.amazon.webservices.awsecommerceservice._2009_07_01.Review> revBuf = new ArrayList<com.amazon.webservices.awsecommerceservice._2009_07_01.Review>();
		if (!resp.getItems().get(0).getItem().isEmpty())
		{
			revBuf.addAll(resp.getItems().get(0).getItem().get(0).getCustomerReviews().getReview());
			int pages = resp.getItems().get(0).getItem().get(0).getCustomerReviews().getTotalReviewPages().intValue();
			pages = MAX_REVIEW_PAGES==0||pages<MAX_REVIEW_PAGES?pages:MAX_REVIEW_PAGES;
			
			for (int i=2; i<pages; i++)
			{
				r.setReviewPage(BigInteger.valueOf(i));
				resp = port.itemLookup(req);
				for (Item thisItem : resp.getItems().get(0).getItem())
					revBuf.addAll(thisItem.getCustomerReviews().getReview());
			}
		}
		
		for (com.amazon.webservices.awsecommerceservice._2009_07_01.Review thisReview : revBuf)
		{
			Review review = new Review();
			review.setTitle(thisReview.getSummary());
			review.setText(thisReview.getContent());
			review.setStars(thisReview.getRating().intValue());
			result.add(review);
		}				

		return result;
	}

	private String findProductDescription(List<EditorialReview> editorialReview) 
	{
		for (EditorialReview review : editorialReview)
		{
			if (review.getSource()!=null && 
					(review.getSource().equals("Product Description") || 
					 review.getSource().equals("Aus der Amazon.de-Redaktion")))
			{
				return review.getContent();
			}
		}
		return null;
	}

	private String findAuthor(Item source) 
	{
		ItemAttributes attrs = source.getItemAttributes();
		if (attrs==null)
			return null;
		
		if (attrs.getAuthor()!=null && attrs.getAuthor().size()>0 && attrs.getAuthor().get(0).trim().length()>0)
			return attrs.getAuthor().get(0).trim();
		else if (attrs.getArtist()!=null && attrs.getArtist().size()>0 && attrs.getArtist().get(0).trim().length()>0)
			return attrs.getArtist().get(0).trim();
		else if (attrs.getLabel()!=null && attrs.getLabel().trim().length()>0)
			return attrs.getLabel().trim();
		else if (attrs.getCreator()!=null && attrs.getCreator().size()>0 && attrs.getCreator().get(0).getValue().trim().length()>0)
			return attrs.getCreator().get(0).getValue().trim();
		else 
			return null;
	}
	
	public static void main(String[] args)
	{
		Amazon amazon = new Amazon();
		//Product l = amazon.lookupProduct("9783897215276", "DE");
		ProductList l = amazon.searchProducts("Harry Potter", "DE");
		System.out.println(l);
	}
}
